package service

import (
	"context"
	"fmt"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/ethereum/go-ethereum/ethclient"
	go_logger "github.com/ethmonitor/go-logger"
	"go.uber.org/zap"
	"math/big"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/ethereum/go-ethereum"
	"github.com/ethereum/go-ethereum/core/types"
)

type Service struct {
	*ethclient.Client

	EthServer        string
	ContractsCreated int64
	TokenTransfers   int64
	ContractCalls    int64
	EthTransfers     int64
	BlockSize        float64
	LoadTime         float64
	TotalEth         *big.Int
	CurrentBlock     *types.Block
	Sync             *ethereum.SyncProgress
	LastBlockUpdate  time.Time
	SugGasPrice      *big.Int
	PendingTx        uint
	NetworkId        *big.Int

	ctx    context.Context
	cancel context.CancelFunc

	addresses map[string]Address
	config    *Config
}

type Address struct {
	Balance *big.Int
	Address string
	Nonce   uint64
}

type Config struct {
	EthServer  string
	HttpServer string
	WatchAddr  []string
	Delay      int
}

func NewService(ctx context.Context, config *Config) *Service {
	c, cancel := context.WithCancel(ctx)
	return &Service{
		ctx:       c,
		cancel:    cancel,
		addresses: make(map[string]Address),
		TotalEth:  big.NewInt(0),
		config:    config,
	}
}

func (s *Service) Start() error {
	defer s.cancel()

	var (
		err  error
		errc = make(chan error)
	)
	s.Client, err = ethclient.DialContext(s.ctx, s.config.EthServer)
	if err != nil {
		go_logger.Logger.Error("DialContext", zap.Error(err))
		return err
	}

	s.CurrentBlock, err = s.Client.BlockByNumber(s.ctx, nil)
	if err != nil {
		go_logger.Logger.Error("BlockByNumber", zap.Error(err))
		return err
	}

	go s.runLoop(errc)
	go s.runHttpLoop(errc)

	select {
	case <-s.ctx.Done():
		go_logger.Logger.Info("Server", zap.String("ctx.Done", "server exit"))
		return nil
	case err, ok := <-errc:
		if ok {
			go_logger.Logger.Error("Server", zap.Error(err))
			return err
		}
	}

	return nil
}

func (s *Service) runHttpLoop(errc chan error) {
	go_logger.Logger.Info("server", zap.String("http server running", fmt.Sprintf("http://%s/metrics", s.config.HttpServer)))
	http.HandleFunc("/metrics", s.MetricsHttp)
	err := http.ListenAndServe(s.config.HttpServer, nil)
	if err != nil {
		errc <- err
	}
}

func (s *Service) CalculateTotals(block *types.Block) {
	s.TotalEth = big.NewInt(0)
	s.ContractsCreated = 0
	s.TokenTransfers = 0
	s.EthTransfers = 0

	for _, tx := range block.Transactions() {
		if tx.To() == nil {
			s.ContractsCreated++
		}

		if len(tx.Data()) >= 4 {
			if method := hexutil.Encode(tx.Data()[:4]); method == "0xa9059cbb" {
				s.TokenTransfers++
			}
		}

		if tx.Value().Sign() == 1 {
			s.EthTransfers++
		}

		s.TotalEth.Add(s.TotalEth, tx.Value())
	}

	size := strings.Split(s.CurrentBlock.Size().String(), " ")
	s.BlockSize = stringToFloat(size[0]) * 1000
}

func (s *Service) MetricsHttp(w http.ResponseWriter, r *http.Request) {
	var allOut []string
	block := s.CurrentBlock
	if block == nil {
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte(fmt.Sprintf("issue receiving block from URL: %v", s.config.EthServer)))
		return
	}

	s.CalculateTotals(block)

	allOut = append(allOut, fmt.Sprintf("geth_block %v", block.NumberU64()))
	allOut = append(allOut, fmt.Sprintf("geth_seconds_last_block %0.2f", time.Now().Sub(s.LastBlockUpdate).Seconds()))
	allOut = append(allOut, fmt.Sprintf("geth_block_transactions %v", len(block.Transactions())))
	allOut = append(allOut, fmt.Sprintf("geth_block_value %v", ToEther(s.TotalEth)))
	allOut = append(allOut, fmt.Sprintf("geth_block_gas_used %v", block.GasUsed()))
	allOut = append(allOut, fmt.Sprintf("geth_block_gas_limit %v", block.GasLimit()))
	allOut = append(allOut, fmt.Sprintf("geth_block_nonce %v", block.Nonce()))
	allOut = append(allOut, fmt.Sprintf("geth_block_difficulty %v", block.Difficulty()))
	allOut = append(allOut, fmt.Sprintf("geth_block_uncles %v", len(block.Uncles())))
	allOut = append(allOut, fmt.Sprintf("geth_block_size_bytes %v", s.BlockSize))
	allOut = append(allOut, fmt.Sprintf("geth_gas_price %v", s.SugGasPrice))
	allOut = append(allOut, fmt.Sprintf("geth_pending_transactions %v", s.PendingTx))
	allOut = append(allOut, fmt.Sprintf("geth_network_id %v", s.NetworkId))
	allOut = append(allOut, fmt.Sprintf("geth_contracts_created %v", s.ContractsCreated))
	allOut = append(allOut, fmt.Sprintf("geth_token_transfers %v", s.TokenTransfers))
	allOut = append(allOut, fmt.Sprintf("geth_eth_transfers %v", s.EthTransfers))
	allOut = append(allOut, fmt.Sprintf("geth_load_time %0.4f", s.LoadTime))

	if s.Sync != nil {
		allOut = append(allOut, fmt.Sprintf("geth_known_states %v", int(s.Sync.KnownStates)))
		allOut = append(allOut, fmt.Sprintf("geth_highest_block %v", int(s.Sync.HighestBlock)))
		allOut = append(allOut, fmt.Sprintf("geth_pulled_states %v", int(s.Sync.PulledStates)))
	}

	for _, v := range s.addresses {
		allOut = append(allOut, fmt.Sprintf("geth_address_balance{address=\"%v\"} %v", v.Address, ToEther(v.Balance).String()))
		allOut = append(allOut, fmt.Sprintf("geth_address_nonce{address=\"%v\"} %v", v.Address, v.Nonce))
	}

	w.Write([]byte(strings.Join(allOut, "\n")))

}

func (s *Service) runLoop(errc chan error) {
	defer s.cancel()
	var (
		lastBlock *types.Block
		err       error
	)
	duration := time.Duration(s.config.Delay) * time.Millisecond
	trace := time.NewTicker(duration)
	defer trace.Stop()

	for {
		select {
		case <-s.ctx.Done():
			return
		case <-trace.C:
			start := time.Now()
			nextID := big.NewInt(s.CurrentBlock.Number().Int64() + 1)
			lastBlock, err = s.Client.BlockByNumber(s.ctx, nextID)
			if err != nil {
				go_logger.Logger.Sugar().Warn("get last block failed", err)
				errc <- err
				return
			}

			if lastBlock == nil || s.CurrentBlock.NumberU64() < lastBlock.NumberU64() {
				s.LastBlockUpdate = time.Now()
				s.LoadTime = time.Now().Sub(start).Seconds()
			} else {
				go_logger.Logger.Sugar().Debug("current block", s.CurrentBlock.NumberU64(), "last block", lastBlock.NumberU64())
				continue
			}

			s.CurrentBlock = lastBlock
			go_logger.Logger.Sugar().Debug("current block", s.CurrentBlock.NumberU64())

			s.SugGasPrice, _ = s.Client.SuggestGasPrice(s.ctx)
			s.PendingTx, _ = s.Client.PendingTransactionCount(s.ctx)
			s.NetworkId, _ = s.Client.NetworkID(s.ctx)
			s.Sync, _ = s.Client.SyncProgress(s.ctx)

			for _, v := range s.config.WatchAddr {
				addr := common.HexToAddress(v)
				balance, _ := s.Client.BalanceAt(s.ctx, addr, s.CurrentBlock.Number())
				nonce, _ := s.Client.NonceAt(s.ctx, addr, s.CurrentBlock.Number())
				address := Address{
					Balance: balance,
					Address: addr.String(),
					Nonce:   nonce,
				}

				s.addresses[address.Address] = address
			}

			trace.Reset(duration)
		}
	}
}

func (s *Service) Close() {
	select {
	case <-s.ctx.Done():
		return
	default:
		s.Client.Close()
		s.cancel()
	}
}

func stringToFloat(s string) float64 {
	amount, _ := strconv.ParseFloat(s, 10)
	return amount
}

func ToEther(o *big.Int) *big.Float {
	pul, int := big.NewFloat(0), big.NewFloat(0)
	int.SetInt(o)
	pul.Mul(big.NewFloat(0.000000000000000001), int)
	return pul
}
